Add root.transient-ro
authorColin Walters <walters@verbum.org>
Wed, 16 Jul 2025 15:08:00 +0000 (11:08 -0400)
committerColin Walters <walters@verbum.org>
Fri, 18 Jul 2025 18:12:09 +0000 (14:12 -0400)
An example use case for this is having privileged code
add dynamic new toplevel mountpoints (that don't persist across
reboots/upgrades), while still keeping the rootfs readonly
for processes by default.

Closes: https://github.com/ostreedev/ostree/issues/3471
Signed-off-by: Colin Walters <walters@verbum.org>
man/ostree-prepare-root.xml
src/libotcore/otcore-prepare-root.c
src/libotcore/otcore.h
src/switchroot/ostree-prepare-root.c
tests/kolainst/destructive/root-transient-ro.sh [new file with mode: 0755]

index 103312ec024bbc2fb39af95385673cc4f9d30776..8627886ba366bb1d415c61194ae5320f5d43a1cc 100644 (file)
@@ -132,6 +132,15 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
                 </para>
                 </listitem>
             </varlistentry>
+            <varlistentry>
+                <term><varname>root.transient-ro</varname></term>
+                <listitem><para>A boolean value; the default is <literal>false</literal>.
+                   This is like <literal>root.transient</literal>, but the overlayfs upper will be mounted
+                   read-only by default. Use this when you want specific privileged components to be able to
+                   write to the upper by temporarily mounting it writable in a new mount namespace.
+                </para>
+                </listitem>
+            </varlistentry>
             <varlistentry>
                 <term><varname>composefs.enabled</varname></term>
                 <listitem><para>This can be <literal>yes</literal>, <literal>no</literal>, <literal>maybe</literal>,
index a52d711d26aa8cf5b8c1c480e3e100a3f8de1005..48ffc317fe553f5065383b9014c6bf452a48f760 100644 (file)
@@ -189,6 +189,18 @@ otcore_load_rootfs_config (const char *cmdline, GKeyFile *config, gboolean load_
   if (!ot_keyfile_get_boolean_with_default (config, ROOT_KEY, OTCORE_PREPARE_ROOT_TRANSIENT_KEY,
                                             FALSE, &ret->root_transient, error))
     return NULL;
+  if (!ot_keyfile_get_boolean_with_default (config, ROOT_KEY, OTCORE_PREPARE_ROOT_TRANSIENT_RO_KEY,
+                                            FALSE, &ret->root_transient_ro, error))
+    return NULL;
+  if (ret->root_transient && ret->root_transient_ro)
+    {
+      return glnx_null_throw (error, "Cannot set both root.transient and root.transient-ro");
+    }
+  // This way callers can test for just root_transient
+  else if (ret->root_transient_ro)
+    {
+      ret->root_transient = TRUE;
+    }
 
   g_autofree char *enabled = g_key_file_get_value (config, OTCORE_PREPARE_ROOT_COMPOSEFS_KEY,
                                                    OTCORE_PREPARE_ROOT_ENABLED_KEY, NULL);
@@ -451,6 +463,8 @@ otcore_mount_rootfs (RootConfig *rootfs_config, GVariantBuilder *metadata_builde
   /* Pass on the state  */
   g_variant_builder_add (metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_ROOT_TRANSIENT,
                          g_variant_new_boolean (rootfs_config->root_transient));
+  g_variant_builder_add (metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_ROOT_TRANSIENT_RO,
+                         g_variant_new_boolean (rootfs_config->root_transient_ro));
 
   bool using_composefs = FALSE;
 #ifdef HAVE_COMPOSEFS
@@ -496,6 +510,8 @@ otcore_mount_rootfs (RootConfig *rootfs_config, GVariantBuilder *metadata_builde
 
       cfs_options.workdir = root_workdir;
       cfs_options.upperdir = root_upperdir;
+      if (rootfs_config->root_transient_ro)
+        cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY;
     }
   else
     {
index 456db9d67f4aa695c20ec443ae55cdf4752328b3..0c7329d326b0265008b5003e0e0ffee7140045ae 100644 (file)
@@ -68,6 +68,7 @@ typedef struct
 {
   OtTristate composefs_enabled;
   gboolean root_transient;
+  gboolean root_transient_ro;
   gboolean require_verity;
   gboolean is_signed;
   char *signature_pubkey;
@@ -132,6 +133,7 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder,
 #define OTCORE_PREPARE_ROOT_ENABLED_KEY "enabled"
 #define OTCORE_PREPARE_ROOT_KEYPATH_KEY "keypath"
 #define OTCORE_PREPARE_ROOT_TRANSIENT_KEY "transient"
+#define OTCORE_PREPARE_ROOT_TRANSIENT_RO_KEY "transient-ro"
 
 // For use with systemd soft reboots
 #define OTCORE_RUN_NEXTROOT "/run/nextroot"
@@ -152,6 +154,8 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder,
 #define OTCORE_RUN_BOOTED_KEY_COMPOSEFS_SIGNATURE "composefs.signed"
 // This key will be present if the root is transient
 #define OTCORE_RUN_BOOTED_KEY_ROOT_TRANSIENT "root.transient"
+// This key will be present if the root is transient readonly
+#define OTCORE_RUN_BOOTED_KEY_ROOT_TRANSIENT_RO "root.transient-ro"
 // This key will be present if the sysroot-ro flag was found
 #define OTCORE_RUN_BOOTED_KEY_SYSROOT_RO "sysroot-ro"
 // Always holds the (device, inode) pair of the booted deployment
index cf3bd1c940b538f25feb26abfe9e083e9b742e35..c7df55d0f860f370e1942c107ec0f294872973f1 100644 (file)
@@ -234,6 +234,11 @@ main (int argc, char *argv[])
   const bool sysroot_currently_writable = !path_is_on_readonly_fs (root_arg);
   g_print ("sysroot.readonly configuration value: %d (fs writable: %d)\n", (int)sysroot_readonly,
            (int)sysroot_currently_writable);
+  if (rootfs_config->root_transient)
+    {
+      g_print ("root.transient: %d (ro: %d)\n", (int)rootfs_config->root_transient,
+               (int)rootfs_config->root_transient_ro);
+    }
 
   /* Remount root MS_PRIVATE here to avoid errors due to the kernel-enforced
    * constraint that disallows MS_SHARED mounts to be moved.
diff --git a/tests/kolainst/destructive/root-transient-ro.sh b/tests/kolainst/destructive/root-transient-ro.sh
new file mode 100755 (executable)
index 0000000..7a8ab54
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -xeuo pipefail
+
+. ${KOLA_EXT_DATA}/libinsttest.sh
+
+prepare_tmpdir
+
+echo "testing boot=${AUTOPKGTEST_REBOOT_MARK:-}"
+
+# Print this by default on each boot
+ostree admin status
+
+case "${AUTOPKGTEST_REBOOT_MARK:-}" in
+  "")
+  # xref https://github.com/coreos/coreos-assembler/pull/2814
+  systemctl mask --now zincati
+
+  test '!' -w /
+
+  cp /usr/lib/ostree/prepare-root.conf /etc/ostree/
+  cat >> /etc/ostree/prepare-root.conf <<'EOF'
+[root]
+transient-ro = true
+EOF
+
+  rpm-ostree initramfs-etc --track /etc/ostree/prepare-root.conf
+  
+  /tmp/autopkgtest-reboot "2"
+  ;;
+  "2")
+
+  test '!' -w '/'
+
+  unshare -m /bin/sh -c 'env LIBMOUNT_FORCE_MOUNT2=always mount -o remount,rw / && mkdir /new-dir-in-root'
+  test -d /new-dir-in-root
+
+  test '!' -w '/'
+
+  echo "ok root transient-ro"
+  ;;
+  *) 
+  fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}" 
+  ;;
+esac